﻿using System;
using System.IO;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using RapidDownloader.Model.Helpers;
using Vidyano;
using Vidyano.Extensions;

namespace RapidDownloader.Model
{
    public abstract class DownloadFile : ViewModelBase, IEquatable<DownloadFile>
    {
        #region Private Fields

        private long[] bandwidthAverager = { 0L, 0L, 0L, 0L, 0L };
        private long deltaBytesSize;
        private IDisposable downloadingSubscriber;

        #endregion

        #region Private Property Backers

        private bool? _CanDownload;
        private TaskProgressState _DownloadState = TaskProgressState.Ready;
        private string _FailReason, _SaveFileName;
        private double _DownloadProgress;
        private long _DownloadedBytes;
        private long _InitialBytes;
        private int _RetryCount;
        private int _MaxRetryCount = 5;
        private bool _CanStartOrResume;
        private double _BandwidthUsage;
        private string _TimeRemaining = "-";

        #endregion

        #region Constructor

        protected DownloadFile()
        {
            Subscriptions.Subscribe(Observable.Interval(TimeSpan.FromSeconds(1)), InvalidateBandwidthUsage);
        }

        #endregion

        #region Properties

        public bool? CanDownload
        {
            get { return _CanDownload; }
            protected set
            {
                _CanDownload = value;
                SendPropertyChanged("CanDownload");
            }
        }

        public string FileName { get; protected set; }

        public string Link { get; protected set; }

        public abstract string Key { get; }

        public string Md5 { get; protected set; }

        public long Size { get; protected set; }

        public string SaveFileName
        {
            get
            {
                return _SaveFileName;
            }
            internal set
            {
                _SaveFileName = value;
                SendPropertyChanged("SaveFileName");
            }
        }

        public TaskProgressState DownloadState
        {
            get
            {
                return _DownloadState;
            }
            protected set
            {
                _DownloadState = value;
                SendPropertyChanged("DownloadState");

                if (value == TaskProgressState.Complete)
                    TimeRemaining = "-";
            }
        }

        public string FailReason
        {
            get
            {
                return _FailReason;
            }
            protected set
            {
                _FailReason = value;
                SendPropertyChanged("FailReason");
            }
        }

        public long DownloadedBytes
        {
            get
            {
                return _DownloadedBytes;
            }
            protected set
            {
                _DownloadedBytes = value;
                SendPropertyChanged("DownloadedBytes");
                SendPropertyChanged("TotalBytes");
            }
        }

        public double DownloadProgress
        {
            get
            {
                return _DownloadProgress;
            }
            protected set
            {
                value = Math.Round(value, 2);
                if (_DownloadProgress != value)
                {
                    _DownloadProgress = value;
                    SendPropertyChanged("DownloadProgress");

                    TimeRemaining = DownloadProgress <= 100d && DownloadState != TaskProgressState.Complete ? (BandwidthUsage != 0d ? TimeSpan.FromSeconds((Size - TotalBytes) / BandwidthUsage).Describe() : "∞") : "-";
                }
            }
        }

        public long InitialBytes
        {
            get { return _InitialBytes; }
            protected set
            {
                _InitialBytes = value;
                SendPropertyChanged("InitialBytes");
                SendPropertyChanged("TotalBytes");
            }
        }

        public long TotalBytes
        {
            get
            {
                return DownloadedBytes + InitialBytes;
            }
        }

        public int RetryCount
        {
            get
            {
                return _RetryCount;
            }
            private set
            {
                _RetryCount = value;
                SendPropertyChanged("RetryCount");
            }
        }

        public int MaxRetryCount
        {
            get
            {
                return _MaxRetryCount;
            }
            protected set
            {
                _MaxRetryCount = value;
                SendPropertyChanged("MaxRetryCount");
            }
        }

        public bool CanStartOrResume
        {
            get
            {
                return _CanStartOrResume;
            }
            private set
            {
                _CanStartOrResume = value;
                SendPropertyChanged("CanStartOrResume");
            }
        }

        public double BandwidthUsage
        {
            get
            {
                return _BandwidthUsage;
            }
            private set
            {
                if (_BandwidthUsage != value)
                {
                    _BandwidthUsage = value;
                    SendPropertyChanged("BandwidthUsage");
                }
            }
        }

        public string TimeRemaining
        {
            get { return _TimeRemaining; }
            private set
            {
                if (_TimeRemaining != value)
                {
                    _TimeRemaining = value;
                    SendPropertyChanged("TimeRemaining");
                }
            }
        }

        public abstract string ProviderIcon { get; }

        #endregion

        #region Command Methods

        public void StartOrResume()
        {
            DownloadState = TaskProgressState.Starting;
            CanStartOrResume = false;

            var downloadTask = new Task(() =>
            {
                if (File.Exists(SaveFileName))
                    InitialBytes = new FileInfo(SaveFileName).Length;

                FailReason = string.Empty;

                if (InitialBytes >= Size)
                {
                    // NOTE: Already downloaded, should have deleted the file before if we wanted to overwrite
                    DownloadState = TaskProgressState.Complete;
                    DownloadProgress = 100d;

                    MessageBus.Send(new FileWriteProgressMessage<DownloadFile>(this, Size, Size));

                    return;
                }

                var req = GetRequest();

                if (InitialBytes > 0)
                    req.AddRange(InitialBytes);

                var response = (HttpWebResponse)req.GetResponse();
                if (response.StatusCode != HttpStatusCode.OK && response.StatusCode != HttpStatusCode.PartialContent)
                {
                    // Something is wrong
                }

                DownloadedBytes = 0;

                DownloadContext.AsyncWriter.CreateFile(this, totalSize: Size, append: InitialBytes > 0);
                var reader = response.GetResponseStream().WithOptionalDebug(Size - InitialBytes).AsyncRead(Math.Min((int)Size, 64 * 1024)); // 64KB or filesize
                downloadingSubscriber = reader.Subscribe(bytes =>
                    {
                        DownloadState = TaskProgressState.InProgress;
                        RetryCount = 0;

                        DownloadedBytes += bytes.Length;
                        deltaBytesSize += bytes.Length;
                        DownloadProgress = (100d / Size) * TotalBytes;

                        MessageBus.Send(new FileDownloadProgressMessage(this, bytes));

                        DownloadContext.AsyncWriter.Add(this, bytes);
                    }, DownloadFailed, DownloadCompleted);
            });

            downloadTask.ContinueWith(t => DownloadFailed(t.Exception.GetTaskException()), TaskContinuationOptions.OnlyOnFaulted);

            downloadTask.Start(TaskScheduler.Default);
        }

        #endregion

        #region Public Methods

        public override void Dispose()
        {
            var ds = downloadingSubscriber;
            if (ds != null)
            {
                DownloadContext.AsyncWriter.FileCompleted(this);
                ds.Dispose();

                downloadingSubscriber = null;
            }

            base.Dispose();
        }

        public bool Equals(DownloadFile other)
        {
            if (SaveFileName == null)
                return other != null && other.FileName == FileName;

            return other != null && other.SaveFileName == SaveFileName;
        }

        public override bool Equals(object obj)
        {
            return Equals(obj as DownloadFile);
        }

        public override int GetHashCode()
        {
            return !string.IsNullOrEmpty(SaveFileName) ? SaveFileName.GetHashCode() : FileName.GetHashCode();
        }

        #endregion

        #region Abstract Methods

        protected abstract HttpWebRequest GetRequest();

        #endregion

        #region Private Methods

        private void InvalidateBandwidthUsage(long n)
        {
            if (DownloadState != TaskProgressState.InProgress)
                return;

            if (bandwidthAverager.All(val => val == 0))
                Enumerable.Range(0, bandwidthAverager.Length).Run(index => bandwidthAverager[index] = deltaBytesSize);
            else
                bandwidthAverager = bandwidthAverager.Skip(1).Concat(EnumerableEx.Return(deltaBytesSize)).ToArray();

            BandwidthUsage = bandwidthAverager.Sum() / (double)bandwidthAverager.Length;
            deltaBytesSize = 0;
        }

        private void DownloadFailed(Exception error)
        {
            downloadingSubscriber = null;

            // NOTE: When using AddRange will come here with connection being closed AFTER file is completed
            DownloadCompleted();
            if (TotalBytes < Size)
            {
                if (++RetryCount < MaxRetryCount)
                {
                    StartOrResume();
                    DownloadState = TaskProgressState.Retrying;
                }
                else
                {
                    FailReason = error.Message;
                    DownloadState = TaskProgressState.Failed;

                    CanStartOrResume = true;
                }
            }
        }

        private void DownloadCompleted()
        {
            DownloadContext.AsyncWriter.FileCompleted(this);
            DownloadState = TaskProgressState.Complete;

            downloadingSubscriber = null;
        }

        #endregion
    }
}